package info.niwota.web.proxy;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

import jvs.peer.util.Log;

import org.apache.commons.pool.impl.GenericKeyedObjectPool;

import x.org.apache.http.ConnectionReuseStrategy;
import x.org.apache.http.Header;
import x.org.apache.http.HttpException;
import x.org.apache.http.HttpHost;
import x.org.apache.http.HttpRequest;
import x.org.apache.http.HttpRequestInterceptor;
import x.org.apache.http.HttpResponse;
import x.org.apache.http.HttpStatus;
import x.org.apache.http.HttpVersion;
import x.org.apache.http.entity.ContentProducer;
import x.org.apache.http.entity.EntityTemplate;
import x.org.apache.http.impl.DefaultConnectionReuseStrategy;
import x.org.apache.http.impl.DefaultHttpClientConnection;
import x.org.apache.http.params.HttpParams;
import x.org.apache.http.params.HttpProtocolParams;
import x.org.apache.http.params.SyncBasicHttpParams;
import x.org.apache.http.protocol.BasicHttpContext;
import x.org.apache.http.protocol.ExecutionContext;
import x.org.apache.http.protocol.HTTP;
import x.org.apache.http.protocol.HttpContext;
import x.org.apache.http.protocol.HttpProcessor;
import x.org.apache.http.protocol.HttpRequestExecutor;
import x.org.apache.http.protocol.HttpRequestHandler;
import x.org.apache.http.protocol.ImmutableHttpProcessor;
import x.org.apache.http.protocol.RequestConnControl;
import x.org.apache.http.protocol.RequestContent;
import x.org.apache.http.protocol.RequestExpectContinue;
import x.org.apache.http.protocol.RequestTargetHost;
import x.org.apache.http.protocol.RequestUserAgent;

public class ProxyHandler implements HttpRequestHandler {
	 private static final String TAG = "ProxyHandler";

	String APPLICATION_PAC = "application/x-ns-proxy-autoconfig";
	 //String APPLICATION_X_JAVASCRIPT_CONFIG = "application/x-javascript-config";
	 
	private final HttpProcessor httpproc;
	private final HttpRequestExecutor httpexecutor;
	private final ConnectionReuseStrategy connStrategy;
	private final HttpParams params;

	public ProxyHandler() {
		super();

		this.params = new SyncBasicHttpParams();
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
		HttpProtocolParams.setContentCharset(params, "UTF-8");
		HttpProtocolParams.setUserAgent(params, "HttpComponents/1.1");
		HttpProtocolParams.setUseExpectContinue(params, true);

		this.httpproc = new ImmutableHttpProcessor(
				new HttpRequestInterceptor[] {
						// Required protocol interceptors
						new RequestContent(), new RequestTargetHost(),
						// Recommended protocol interceptors
						new RequestConnControl(), new RequestUserAgent(),
						new RequestExpectContinue() });

		this.httpexecutor = new HttpRequestExecutor();

		//
		this.connStrategy = new DefaultConnectionReuseStrategy();
	}

	public void handle(final HttpRequest request,
			final HttpResponse response, final HttpContext context)
			throws HttpException, IOException {
		
		if (Log.DEBUG) {
			Log.d(TAG,">> Request URI: "
				+ request.getRequestLine().getUri());
		}
		if (Log.TRACE) {
			print(">>>>> ", request.getAllHeaders());
		}

		final boolean keepalive = this.connStrategy.keepAlive(response,
				context);

		if (Log.DEBUG) {
			Log.d(TAG,"-- incoming request line: "
				+ request.getRequestLine() + " method: "
				+ request.getRequestLine().getMethod() + " keep alive: "
				+ keepalive);
		}
		
		//handle if special uri 
		if (!request.getRequestLine().getMethod().equals("CONNECT")) {
			try {
				
				Uri uri = new Uri(request.getRequestLine().getUri());
				
				if (Log.DEBUG) {
					Log.d(TAG,"parsed uri: " + uri);
				}
				
				if (handlePac(uri, context, request, response)) {
					return;
				}
			
				if (handleLocal(uri, context, request, response)) {
					return;
				}
				
				if (handleConnect(uri, context, request, response)) {
					return;
				}
				
				if (uri.getScheme() == null || uri.getHost() == null) {
					sendNotFound(uri, context, request, response);
					return;
				}
				//TODO handle xmpp
			} catch (Exception e) {
				//ignore and continue normal processing e.g  due to invalid char encoding etc
				e.printStackTrace();
			}
		}
		//
		
		// proxy
		try {
			handle(context, request, response);
		} catch (Exception e) {
			handleError(request, response, context, e);

			e.printStackTrace();
		}
	}
	
	private void sendNotFound(final Uri uri, HttpContext context,
			HttpRequest request, HttpResponse response) {
		response.setStatusCode(HttpStatus.SC_OK);
		EntityTemplate body = new EntityTemplate(new ContentProducer() {

			public void writeTo(final OutputStream outstream)
					throws IOException {
				OutputStreamWriter w = new OutputStreamWriter(outstream,
						"UTF-8");
				w.write("Not found " + uri);
				w.flush();
			}
		});
		body.setContentType("text/html");
		response.setEntity(body);
	}

	protected boolean isLocal(Uri uri) {
		final String host = uri.getHost();
		final boolean local = (host == null || host.equalsIgnoreCase("localhost") || host.equals("127.0.0.1"));
		return (local);
	}

	protected boolean handlePac(Uri uri, HttpContext context,
			HttpRequest request, HttpResponse response) throws Exception {
		
		if (!isLocal(uri)) {
			return false;
		}
		
		final String realm = uri.getParameter("realm");
		final GenericKeyedObjectPool pool = PeerRoute.get(realm);
		final String address = uri.getParameter("address");
		final String path = uri.getPath();
		
		if (realm == null || address == null) {
			return false;
		}
		
		if (!path.equalsIgnoreCase("/proxy.pac")) {
			return false;
		}
		
		if (Log.DEBUG) {
			Log.d(TAG,"handlePac " + uri +" pool " + pool + " realm: " + realm + " address: " + address +" path: "  + path);
		}
		//
		ServerSocketThread obj = null;
		try {
			obj = (ServerSocketThread) pool.borrowObject(address);
			
			final String proxy = "localhost:" + obj.getPort();
			
			response.setStatusCode(HttpStatus.SC_OK);
			EntityTemplate body = new EntityTemplate(new ContentProducer() {

				public void writeTo(final OutputStream outstream)
						throws IOException {
					OutputStreamWriter w = new OutputStreamWriter(outstream,
							"UTF-8");
					w.write("function FindProxyForURL(url, host) {\r\n");
					w.write("  if (shExpMatch(host, \"127.0.0.*\") || shExpMatch(host, \"localhost\")) { return \"DIRECT\"; }\r\n");
					w.write("  return \"PROXY " + proxy+"; DIRECT\";\r\n");
					w.write("}\r\n");
					w.flush();
				}
			});
			body.setContentType(APPLICATION_PAC);
			response.setEntity(body);
			
			if (Log.DEBUG) {
				Log.d(TAG,"proxy url: " + proxy);
			}
		} catch (Exception e) {
			if (obj != null) {
				pool.invalidateObject(address, obj);
			}
			
			e.printStackTrace();
		} finally {
			if (obj != null) {
				pool.returnObject(address, obj);
			}
		}
		
		return true;
	}
	
	protected boolean handleConnect(Uri uri, HttpContext context,
			HttpRequest request, HttpResponse response) throws Exception {
		if (!isLocal(uri)) {
			return false;
		}
		
		final String realm = uri.getParameter("realm");
		final GenericKeyedObjectPool pool = PeerRoute.get(realm);
		final String address = uri.getParameter("address");
		final String path = uri.getPath();
		
		if (realm == null || address == null) {
			return false;
		}
		
		if (!path.equalsIgnoreCase("/bin/connect")) {
			return false;
		}
		
		if (Log.DEBUG) {
			Log.d(TAG,"handleConnect "+ uri + " pool " + pool + " realm: " + realm + " address: " + address );
		}
		//
		ServerSocketThread obj = null;
		try {
			obj = (ServerSocketThread) pool.borrowObject(address);
			
			final String location = "http://localhost:" + obj.getPort();
			
			response.setStatusCode(HttpStatus.SC_MOVED_TEMPORARILY);
			response.setHeader("Location", location);
			response.setHeader("Connection", "Close");
			
			if (Log.DEBUG) {
				Log.d(TAG,"redirecting to: " + location);
			}
		} catch (Exception e) {
			if (obj != null) {
				pool.invalidateObject(address, obj);
			}
			
			e.printStackTrace();
		} finally {
			if (obj != null) {
				pool.returnObject(address, obj);
			}
		}
		
		return true;
	}

	private void handleError(final HttpRequest request,
			final HttpResponse response, final HttpContext context,
			final Exception e) {
		response.setStatusCode(HttpStatus.SC_OK);
		EntityTemplate body = new EntityTemplate(new ContentProducer() {

			public void writeTo(final OutputStream outstream)
					throws IOException {
				OutputStreamWriter w = new OutputStreamWriter(outstream,
						"UTF-8");
				w.write("<html><body><h1>Error</h1><br /> <p>");
				w.write(e.getLocalizedMessage());
				w.write("</p></body></html>");
				w.flush();
			}

		});
		body.setContentType("text/html; charset=UTF-8");
		response.setEntity(body);
	}

	protected boolean handleLocal(final Uri uri, final HttpContext context,
			final HttpRequest request, final HttpResponse response)
			throws Exception {
		
		return false;
	}
	


	private void handle(final HttpContext context,
			final HttpRequest request, final HttpResponse response)
			throws HttpException, IOException {

		Header[] hd = request.getHeaders("Proxy-Authorization");
		
		if (hd != null && hd.length > 0) {
			Log.d(TAG,"proxy authorization: " + hd[0].getName()
					+ ":" + hd[0].getValue());
		}
		
		//proxy
		if (request.getRequestLine().getMethod().equals("CONNECT")) {
			tunnelRequest(request, response, (ProxyHttpContext) context);
		} else {
			proxyRequest(request, response, (ProxyHttpContext) context);
		}
	}

	protected void tunnelRequest(final HttpRequest request,
			final HttpResponse response, final ProxyHttpContext context) {

		try {
			ProxyHttpServerConnection conn = (ProxyHttpServerConnection) context
					.getAttribute(ExecutionContext.HTTP_CONNECTION);

			conn.setTunneling(true);

			//
			String[] hostport = request.getRequestLine().getUri()
					.split(":");

			Socket outsocket = null;

			outsocket = new Socket(hostport[0],
					Integer.parseInt(hostport[1]));

			Thread t1 = new CopyInThread(conn.getIn(),
					outsocket.getOutputStream());

			Thread t2 = new CopyOutThread(outsocket.getInputStream(),
					conn.getOut());

			t1.setDaemon(true);
			t2.setDaemon(true);

			t1.start();
			t2.start();

			response.setStatusLine(request.getProtocolVersion(),
					HttpStatus.SC_OK, "Connection established");
			response.setHeader("Proxy-Agent", "Proxy/1.0");

			if (Log.DEBUG) {
				Log.d(TAG,hostport[0] + ":" + hostport[1]
					+ " started threads for ssl : " + t1 + " " + t2);
			}
			
			conn.flush();
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	protected void proxyRequest(final HttpRequest request,
			final HttpResponse response, ProxyHttpContext context) {
		final BasicHttpContext pctx = context.context;
		final DefaultHttpClientConnection pconn = context.conn;

		try {
			final ProxyRequestLine requestLine = new ProxyRequestLine(
					request.getRequestLine());

			if (Log.DEBUG) {
				Log.d(TAG,"modified request line: " + requestLine
					+ " original: " + request.getRequestLine());
			}
			
			if (!pconn.isOpen() && requestLine.isAbsolute()) {
				// pass on user agent
				Header head = request.getLastHeader("User-Agent");
				String userAgent = null;
				if (head != null) {
					userAgent = head.getValue();
				}
				HttpProtocolParams.setUserAgent(this.params,
						userAgent == null ? "HttpComponents/1.1"
								: userAgent);
				//
				final String host = requestLine.getHost();
				final int port = requestLine.getPort();

				HttpHost httphost = new HttpHost(host, port);
				Socket socket = new Socket(host, port);
				pconn.bind(socket, this.params);

				pctx.setAttribute(ExecutionContext.HTTP_CONNECTION, pconn);
				pctx.setAttribute(ExecutionContext.HTTP_TARGET_HOST,
						httphost);
			}

			//
			request.removeHeaders(HTTP.CONTENT_LEN);
			request.removeHeaders(HTTP.TRANSFER_ENCODING);
			request.removeHeaders(HTTP.CONN_DIRECTIVE);
			request.removeHeaders("Keep-Alive");
			request.removeHeaders("TE");
			request.removeHeaders("Trailers");
			request.removeHeaders("Upgrade");

			request.removeHeaders("Proxy-Connection");
			request.removeHeaders("Proxy-Authorization");
			request.removeHeaders("Proxy-Authenticate");

			request.setHeader(HTTP.CONN_DIRECTIVE, "Close");

			// modify request line
			if (requestLine.isAbsolute()) {
				request.setRequestLine(requestLine);
			}

			//
			if (Log.DEBUG) {
				print("modified >>>>  ", request.getAllHeaders());
			}
			
			//
			httpexecutor.preProcess(request, this.httpproc, pctx);

			HttpResponse targetResponse = httpexecutor.execute(request,
					pconn, pctx);

			httpexecutor.postProcess(response, this.httpproc, pctx);

			if (targetResponse.getStatusLine().getStatusCode() > 400) {
				Log.d(TAG,targetResponse.getStatusLine()
						.toString());
			}

			if (Log.DEBUG) {
				print("<<<<< ", targetResponse.getAllHeaders());
			}
			//

			targetResponse.removeHeaders(HTTP.CONTENT_LEN);
			targetResponse.removeHeaders(HTTP.TRANSFER_ENCODING);
			targetResponse.removeHeaders(HTTP.CONN_DIRECTIVE);
			targetResponse.removeHeaders("Keep-Alive");
			targetResponse.removeHeaders("TE");
			targetResponse.removeHeaders("Trailers");
			targetResponse.removeHeaders("Upgrade");

			targetResponse.setHeader(HTTP.CONN_DIRECTIVE, "Close");
			//
			response.setStatusLine(targetResponse.getStatusLine());
			response.setHeaders(targetResponse.getAllHeaders());
			response.setEntity(targetResponse.getEntity());

			if (Log.DEBUG) {
				Log.d(TAG,"<< << Outgoing Response: "
					+ response.getStatusLine());
			}
			if (Log.TRACE) {
				print("<<<<< ", targetResponse.getAllHeaders());
			}

			//
			ProxyHttpServerConnection conn = (ProxyHttpServerConnection) context
					.getAttribute(ExecutionContext.HTTP_CONNECTION);

			pconn.flush();
			conn.flush();
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	private void print(String tag, Header[] allHeaders) {
		for (int i = 0; i < allHeaders.length; i++) {
			Header h = allHeaders[i];
			Log.d(tag, h.getName() + ": " + h.getValue());
		}
	}

}
